import struct
import pkcrypt
import hashlib
import io
import time

class ValidateDataError(Exception): pass
class Panic(Exception): pass
    
max_magnetlen = 500
max_titlelen = 160
max_creatorlen = 20
max_textlen = 160    
max_pubkeylen = 100
max_privkeylen = 1000
max_siglen = 100
max_entrylen = (4 + max_magnetlen +
                4 + max_titlelen +
                4 + max_creatorlen +
                4 + max_textlen +
                4 + 8 + 4 + 8 +
                4 + max_pubkeylen +
                4 + max_siglen)

categories = {"AudioMusic": 0,
             "AudioAudiobooks": 1,
             "AudioSoundclips": 2,
             "AudioFlac": 3,
             "AudioOther": 4,
             "VideoMovies": 5,
             "VideoMoviesdvdr": 6,
             "VideoMusicvideos": 7,
             "VideoMovieclips": 8,
             "VideoTvshows": 9,
             "VideoHandheld": 10,
             "VideoHighresmovies": 11,
             "VideoHighrestvshows": 12,
             "VideoOther": 13,
             "ApplicationsWindows": 14,
             "ApplicationsMac": 15,
             "ApplicationsUnix": 16,
             "ApplicationsHandheld": 17,
             "ApplicationsOther": 18,
             "GamesPc": 19,
             "GamesMac": 20,
             "GamesPs2": 21,
             "GamesXbox360": 22,
             "GamesWii": 23,
             "GamesHandheld": 24,
             "GamesOther": 25,
             "PornMovies": 26,
             "PornMoviedvdr": 27,
             "PornPictures": 28,
             "PornGames": 29,
             "PornHighresmovies": 30,
             "PornMovieclips": 31,
             "PornOther": 32,
             "OtherEbooks": 33,
             "OtherComics": 34,
             "OtherPictures": 35,
             "OtherCovers": 36,
             "OtherOther": 37}

categories2main = {
    "AudioMusic": "Audio",
    "AudioAudiobooks": "Audio",
    "AudioSoundclips": "Audio",
    "AudioFlac": "Audio",
    "AudioOther": "Audio",
    "VideoMovies": "Video",
    "VideoMoviesdvdr": "Video",
    "VideoMusicvideos": "Video",
    "VideoMovieclips": "Video",
    "VideoTvshows": "Video",
    "VideoHandheld": "Video",
    "VideoHighresmovies": "Video",
    "VideoHighrestvshows": "Video",
    "VideoOther": "Video",
    "ApplicationsWindows": "Applications",
    "ApplicationsMac": "Applications",
    "ApplicationsUnix": "Applications",
    "ApplicationsHandheld": "Applications",
    "ApplicationsOther": "Applications",
    "GamesPc": "Games",
    "GamesMac": "Games",
    "GamesPs2": "Games",
    "GamesXbox360": "Games",
    "GamesWii": "Games",
    "GamesHandheld": "Games",
    "GamesOther": "Games",
    "PornMovies": "Porn",
    "PornMoviedvdr": "Porn",
    "PornPictures": "Porn",
    "PornGames": "Porn",
    "PornHighresmovies": "Porn",
    "PornMovieclips": "Porn",
    "PornOther": "Porn",
    "OtherEbooks": "Other",
    "OtherComics": "Other",
    "OtherPictures": "Other",
    "OtherCovers": "Other",
    "OtherOther": "Other"}
    
main_categories = set(categories2main.values())

def get_category_str(cat):
    for key, value in categories.items():
        if cat == value:
            return key
    return "OtherOther"

def get_categories_from_main(main_cat):
    r = []
    for key, value in categories.items():
        if main_cat == categories2main[key]:
            r.append(value)
    return r

def validate_ip(ip):
    #if ip[0] == 127:
    #    raise ValidateDataError("Loopback ip not ok")
    if ip[0] == 0:
        raise ValidateDataError("ip cant start with zero")
    if ip[3] == 0:
        raise ValidateDataError("ip cant end with zero")
    if ip[3] == 255:
        raise ValidateDataError("ip cant end with 255")

    return 
    
def validate_port(port):
    if port == 0:
        raise ValidateDataError("port == 0")

def validate_magnet(magnet):
    pass

def validate_time(t):
    if t > int(time.mktime(time.gmtime()))+3600:
        raise ValidateDataError("The timestamp was in the future")    


def delete_peer(db, peerid):
    db.execute("BEGIN")
    db.execute("DELETE FROM peers WHERE id=?", (peerid, ) )
    db.execute("DELETE FROM votes WHERE peer=?", (peerid, ) )
    db.commit()

def generate_entry(magnet, title, category, size, num_files, created,
                   text, keypair):
    data = b""
    data += struct.pack("I", len(magnet.encode("utf-8")))
    data += magnet.encode("utf-8")
    data += struct.pack("I", len(title.encode("utf-8")))
    data += title.encode("utf-8")
    data += struct.pack("I", len(keypair["name"].encode("utf-8")))
    data += keypair["name"].encode("utf-8")
    data += struct.pack("I", len(text.encode("utf-8")))
    data += text.encode("utf-8")
    data += struct.pack("I", category)
    data += struct.pack("Q", size)
    data += struct.pack("I", num_files)
    data += struct.pack("q", created)
    data += struct.pack("I", len(keypair["public"]))
    data += keypair["public"]

    # Finally sign it
    sig = pkcrypt.sign(keypair["private"], hashlib.sha1(data).digest())
    data += struct.pack("I", len(sig))
    data += sig

    # Validate and return
    validate_entry(data)
    return data

class HashBytesIO:
    def __init__(self, data):
        self.ds = io.BytesIO(data)
        self.hash = hashlib.sha1()
    
    def read(self, n):
        r = self.ds.read(n)
        if len(r) != n:
            raise ValidateDataError("Unexpected end of data")
        self.hash.update(r)
        return r
    
    def digest(self):
        return self.hash.digest()

def validate_entry(data):
    parse_entry(data)

def parse_entry(data):
    hs = HashBytesIO(data)
    try:
        magnetlen, = struct.unpack("I", hs.read(4))
        if magnetlen > max_magnetlen:
            raise ValidateDataError("Magnet link longer then %d"%(max_magnetlen,))
        magnet = hs.read(magnetlen).decode("utf-8")
        validate_magnet(magnet)
        titlelen, = struct.unpack("I", hs.read(4))
        if titlelen > max_titlelen:
            raise ValidateDataError("Title longer then %d"%(max_titlelen,))
        title = hs.read(titlelen).decode("utf-8")
        creatorlen, = struct.unpack("I", hs.read(4))
        if creatorlen > max_creatorlen:
            raise ValidateDataError("Creator longer then %d"%(max_creatorlen,))
        creator = hs.read(creatorlen).decode("utf-8")
        textlen, = struct.unpack("I", hs.read(4))
        if textlen > max_textlen:
            raise ValidateDataError("Text longer then %d"%(max_textlen,))
        text = hs.read(textlen).decode("utf-8")
    except UnicodeDecodeError:
        raise ValidateDataError("Couldn't decode unicode string")
        
    category, = struct.unpack("I", hs.read(4))
    size, = struct.unpack("Q", hs.read(8))
    num_files, = struct.unpack("I", hs.read(4))
    created, = struct.unpack("q", hs.read(8))
    validate_time(created)
    pubkeylen, = struct.unpack("I", hs.read(4))
    if pubkeylen > max_pubkeylen:
        raise ValidateDataError("Pubkey longer then %d"%(max_pubkeylen,))
    pubkey = hs.read(pubkeylen)
    
    hash = hs.digest()
    siglen, = struct.unpack("I", hs.read(4))
    if siglen > max_siglen:
        raise ValidateDataError("Signature longer then %d"%(max_siglen,))
    sig = hs.read(siglen)
    try:
        if not pkcrypt.verify(pubkey, sig, hash):
            raise ValidateDataError("The signature was incorrect")
    except OpenSSLDataError:
        raise ValidateDataError("OpenSSL couldn't parse the key or the signature")
    
    return {"magnet": magnet,
            "title": title,
            "creator": creator,
            "text": text,
            "category": category,
            "size": size,
            "num_files": num_files,
            "created": created,
            "author": hashlib.sha1(pubkey).digest()}

def serialize_key_pair(keypair):
    d = b""
    d += struct.pack("I", len(keypair["public"]))
    d += keypair["public"]
    d += struct.pack("I", len(keypair["private"]))
    d += keypair["private"]
    d += struct.pack("I", len(keypair["name"].encode("utf-8")))
    d += keypair["name"].encode("utf-8")

    return d
    
def unserialize_key_pair(data):
    r = {}
    s = HashBytesIO(data)
    
    pubkeylen, = struct.unpack("I", s.read(4))
    if pubkeylen > max_pubkeylen:
        ValidateDataError("Too long public key")
    r["public"] = s.read(pubkeylen)
    privkeylen, = struct.unpack("I", s.read(4))
    if privkeylen > max_privkeylen:
        ValidateDataError("Too long private key")
    r["private"] = s.read(privkeylen)
    namelen, = struct.unpack("I", s.read(4))
    if namelen > max_creatorlen:
        ValidateDataError("Too long creator name")
    r["name"] = s.read(namelen).decode("utf-8")
    
    return r
    
def insert_entry(db, entry):
    hash = hashlib.sha1(entry).digest()
    now = int(  time.mktime( time.gmtime() )  )
    entry_info = parse_entry(entry)
    
    db.execute("BEGIN IMMEDIATE")
    # Insert into entries
    entry_exist, = db.execute("SELECT COUNT(*) FROM "+
                              "entries WHERE hash=?", (hash,)).fetchone()
    if entry_exist == 1:
        db.commit()
        return
    db.execute("INSERT INTO entries (data, hash, first_seen) "+
               "VALUES (?, ?, ?)", (entry, hash, now ) )
    entryid, = db.execute("SELECT last_insert_rowid()").fetchone()
    
    # Insert into entries_info
    db.execute("INSERT INTO entries_info (entry, magnet, title, "+
               "creator, text, category, size, num_files, created, "+
               "author, rating, author_rating) VALUES "+
               "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (
               entryid,
               entry_info["magnet"],
               entry_info["title"],
               entry_info["creator"],
               entry_info["text"],
               entry_info["category"],
               entry_info["size"],
               entry_info["num_files"],
               entry_info["created"],
               entry_info["author"],
               0, 0))
               
    # Insert into fts
    cat = get_category_str(entry_info["category"])
    db.execute("INSERT INTO entries_fts_%s (rowid, title) "%(cat,)+
               "VALUES (?, ?)", (entryid, entry_info["title"]))
    main_cat = categories2main[cat]
    db.execute("INSERT INTO entries_fts_%s_all (rowid, title) "%(main_cat,)+
               "VALUES (?, ?)", (entryid, entry_info["title"]))
    db.execute("INSERT INTO entries_fts_all (rowid, title) "+
               "VALUES (?, ?)", (entryid, entry_info["title"]))
    db.commit()

def insert_votes(db, peerid, votes):
    db.execute("BEGIN IMMEDIATE")
    # Remove old votes
    for value, entry, author in db.execute(
        "SELECT votes.value, votes.entry, entries_info.author FROM votes "+
        "INNER JOIN entries_info ON entries_info.entry=votes.entry "+
        "WHERE votes.peer=?", (peerid,)):
        db.execute("UPDATE entries_info SET rating = rating - ? WHERE entry=?",
                   (value, entry))
        db.execute("UPDATE entries_info SET "+
                   "author_rating = author_rating - ? WHERE author=?",
                   (value, author))
    db.execute("DELETE FROM votes WHERE peer=?", (peerid, ) )
    
    # Insert new votes
    for vote in votes:
        entry = db.execute(
            "SELECT entries.id, entries_info.author FROM entries "+
            "INNER JOIN entries_info ON entries_info.entry = entries.id "+
            "WHERE entries.hash=?", (vote["entry"],)).fetchone()
        if not entry:
            continue
        db.execute("INSERT INTO votes (peer, entry, value, time) "+
                   "VALUES (?, ?, ?, ?)", 
                   (peerid, entry[0], vote["value"], vote["time"]))
        db.execute("UPDATE entries_info SET rating = rating + ? WHERE entry=?",
                   (vote["value"], entry[0]))
        db.execute("UPDATE entries_info SET "+
                   "author_rating = author_rating + ? WHERE author=?",
                   (vote["value"], entry[1]))
    db.commit()

def search(db, category, search_string, order_by, asc, page):
    results_per_page = 3
    
    if category == "all":
        ftsq = "SELECT rowid FROM entries_fts_all WHERE title MATCH ?"
    elif category in main_categories:
        ftsq = ("SELECT rowid FROM entries_fts_%s_all "+
                "WHERE title MATCH ?"%(category,))
    elif category in categories:
        ftsq = ("SELECT rowid FROM entries_fts_%s "+
                "WHERE title MATCH ?"%(category,))
    else:
        raise Panic("Bad category in search")
    
    q = ("SELECT * FROM entries_info WHERE id IN (%s) "%(ftsq,)+
         "ORDER BY %s %s "%(order_by, asc)+
         "LIMIT %d OFFSET %d"%(results_per_page+1, results_per_page*page))
    return db.execute(q, (search_string,))
    